Skip to main content
Glama
+page.svelte14.4 kB
<script> import { page } from '$app/stores'; import { onMount } from 'svelte'; let prd = null; let linkedDesigns = []; let linkedDocuments = []; let linkedTasks = { direct: [], indirect: [], all: [] }; let taskStatistics = { total: 0, direct: 0, indirect: 0, completed: 0, progress: 0 }; let loading = true; let error = null; function formatDate(dateValue) { if (!dateValue) return '-'; try { let date; // ISO 문자열 형식인지 확인 (예: 2025-09-05T10:23:42.534Z) if (typeof dateValue === 'string' && dateValue.includes('T')) { date = new Date(dateValue); } // Unix timestamp 형식인지 확인 (예: 1757249412158.0) else if (typeof dateValue === 'string' && /^\d+\.?\d*$/.test(dateValue)) { date = new Date(parseFloat(dateValue)); } // 이미 숫자인 경우 else if (typeof dateValue === 'number') { date = new Date(dateValue); } // 기타 경우 직접 파싱 시도 else { date = new Date(dateValue); } // 유효한 날짜인지 확인 if (isNaN(date.getTime())) { return '-'; } // 날짜와 시간을 모두 표시 return date.toLocaleString('ko-KR', { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }); } catch (error) { console.error('Date formatting error:', error, dateValue); return '-'; } } onMount(async () => { try { // PRD 기본 정보, 연결된 설계, 연결된 작업을 병렬로 로드 const [prdResponse, designsResponse, tasksResponse] = await Promise.all([ fetch(`/api/prds/${$page.params.id}`), fetch(`/api/prds/${$page.params.id}/designs`), fetch(`/api/prds/${$page.params.id}/tasks`) ]); if (prdResponse.ok) { prd = await prdResponse.json(); } else { error = 'PRD를 찾을 수 없습니다'; } if (designsResponse.ok) { const designsData = await designsResponse.json(); linkedDesigns = designsData.designs || []; } if (tasksResponse.ok) { const tasksData = await tasksResponse.json(); linkedTasks = tasksData.tasks || { direct: [], indirect: [], all: [] }; taskStatistics = tasksData.statistics || { total: 0, direct: 0, indirect: 0, completed: 0, progress: 0 }; } } catch (e) { console.error('Data loading error:', e); error = '데이터를 불러오는 중 오류가 발생했습니다'; } finally { loading = false; } }); </script> <svelte:head> <title>{prd?.title || 'PRD 상세보기'} - WorkflowMCP</title> </svelte:head> <div class="max-w-4xl mx-auto space-y-6"> <div class="flex items-center justify-between"> <div> <h1 class="text-3xl font-bold text-gray-900">PRD 상세보기</h1> <p class="text-gray-600 mt-1">프로젝트 요구사항 문서 상세 정보</p> </div> <div class="flex space-x-3"> <a href="/prds" class="btn btn-secondary">← 목록으로</a> {#if prd} <a href="/prds/{prd.id}/edit" class="btn btn-primary">편집</a> {/if} </div> </div> {#if loading} <div class="flex justify-center py-12"> <div class="text-gray-500">데이터를 불러오는 중...</div> </div> {:else if error} <div class="bg-red-50 border border-red-200 rounded-md p-4"> <div class="text-red-800">{error}</div> </div> {:else if prd} <div class="space-y-6"> <!-- 기본 정보 --> <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">기본 정보</h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label class="block text-sm font-medium text-gray-700 mb-1">제목</label> <div class="text-gray-900 font-medium">{prd.title}</div> <div class="text-xs text-gray-500 mt-1 font-mono">ID: {prd.id}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">우선순위</label> <span class="badge {prd.priority === 'high' ? 'bg-red-100 text-red-800' : prd.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-gray-100 text-gray-800'}"> {prd.priority === 'high' ? '높음' : prd.priority === 'medium' ? '보통' : '낮음'} </span> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">상태</label> <span class="badge {prd.status === 'active' ? 'bg-green-100 text-green-800' : prd.status === 'completed' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'}"> {prd.status === 'active' ? '활성' : prd.status === 'completed' ? '완료' : prd.status === 'draft' ? '초안' : prd.status} </span> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">생성일</label> <div class="text-gray-600">{formatDate(prd.created_at)}</div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">최종 수정일</label> <div class="text-gray-600">{formatDate(prd.updated_at)}</div> </div> </div> {#if prd.description} <div class="mt-4"> <label class="block text-sm font-medium text-gray-700 mb-1">설명</label> <div class="text-gray-900 whitespace-pre-wrap">{prd.description}</div> </div> {/if} </div> <!-- 요구사항 --> {#if prd.requirements && prd.requirements.length > 0} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">요구사항</h2> <div class="space-y-3"> {#each prd.requirements as requirement, index} <div class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg"> <span class="text-sm text-gray-500 font-medium min-w-0">{index + 1}.</span> <span class="text-sm text-gray-700"> {typeof requirement === 'string' ? requirement : (requirement.title || requirement.description)} </span> </div> {/each} </div> </div> {/if} <!-- 인수 조건 --> {#if prd.acceptance_criteria && prd.acceptance_criteria.length > 0} <div class="card"> <h2 class="text-xl font-semibold text-gray-900 mb-4">인수 조건</h2> <div class="space-y-3"> {#each prd.acceptance_criteria as criteria, index} <div class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg"> <span class="text-sm text-gray-500 font-medium min-w-0">{index + 1}.</span> <span class="text-sm text-gray-700">{criteria}</span> </div> {/each} </div> </div> {/if} <!-- 연결된 설계 --> {#if linkedDesigns && linkedDesigns.length > 0} <div class="card"> <div class="flex items-center justify-between mb-4"> <h2 class="text-xl font-semibold text-gray-900">📐 연결된 설계 ({linkedDesigns.length}개)</h2> </div> <div class="grid gap-3"> {#each linkedDesigns as design} <div class="p-4 bg-gray-50 rounded-lg border hover:bg-gray-100 transition-colors"> <div class="flex items-start justify-between"> <div class="flex-1"> <div class="flex items-center space-x-2 mb-2"> <span class="text-lg">{design.typeIcon || '📋'}</span> <h3 class="font-medium text-gray-900"> <a href="/designs/{design.id}" class="text-blue-600 hover:text-blue-800 hover:underline"> {design.title} </a> </h3> </div> <div class="flex items-center space-x-2 mb-2"> <span class="badge badge-status-{design.statusColor}"> {design.status === 'draft' ? '초안' : design.status === 'review' ? '검토중' : design.status === 'approved' ? '승인' : design.status === 'implemented' ? '구현완료' : design.status} </span> <span class="badge badge-gray"> {design.design_type === 'system' ? '시스템' : design.design_type === 'architecture' ? '아키텍처' : design.design_type === 'ui_ux' ? 'UI/UX' : design.design_type === 'database' ? '데이터베이스' : design.design_type === 'api' ? 'API' : design.design_type} </span> </div> {#if design.description} <p class="text-gray-600 text-sm"> {design.description.length > 100 ? design.description.substring(0, 100) + '...' : design.description} </p> {/if} {#if design.daysFromLastUpdate !== null} <div class="text-xs text-gray-500 mt-2"> {design.daysFromLastUpdate === 0 ? '오늘 수정됨' : `${design.daysFromLastUpdate}일 전 수정됨`} </div> {/if} </div> </div> </div> {/each} </div> </div> {/if} <!-- 연결된 작업 --> {#if taskStatistics.total > 0} <div class="card"> <div class="flex items-center justify-between mb-4"> <h2 class="text-xl font-semibold text-gray-900">📋 연결된 작업 ({taskStatistics.total}개)</h2> <div class="flex items-center space-x-4"> <div class="text-sm text-gray-600"> 진행률: <span class="font-semibold text-blue-600">{taskStatistics.progress}%</span> </div> <div class="w-24 bg-gray-200 rounded-full h-2"> <div class="bg-blue-600 h-2 rounded-full" style="width: {taskStatistics.progress}%"></div> </div> </div> </div> <!-- 통계 요약 --> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4 p-4 bg-gray-50 rounded-lg"> <div class="text-center"> <div class="text-lg font-semibold text-gray-900">{taskStatistics.total}</div> <div class="text-sm text-gray-600">전체</div> </div> <div class="text-center"> <div class="text-lg font-semibold text-blue-600">{taskStatistics.direct}</div> <div class="text-sm text-gray-600">직접 연결</div> </div> <div class="text-center"> <div class="text-lg font-semibold text-purple-600">{taskStatistics.indirect}</div> <div class="text-sm text-gray-600">간접 연결</div> </div> <div class="text-center"> <div class="text-lg font-semibold text-green-600">{taskStatistics.completed}</div> <div class="text-sm text-gray-600">완료</div> </div> </div> <!-- 직접 연결 작업 --> {#if linkedTasks.direct && linkedTasks.direct.length > 0} <div class="mb-6"> <h3 class="text-lg font-medium text-gray-900 mb-3">직접 연결 작업 ({linkedTasks.direct.length}개)</h3> <div class="grid gap-3"> {#each linkedTasks.direct as task} <div class="p-3 bg-blue-50 rounded-lg border border-blue-200"> <div class="flex items-center justify-between"> <div class="flex-1"> <h4 class="font-medium text-gray-900"> <a href="/tasks/{task.id}" class="text-blue-700 hover:text-blue-900 hover:underline"> {task.title} </a> </h4> <div class="flex items-center space-x-2 mt-1"> <span class="badge {task.status === 'done' || task.status === 'completed' ? 'badge-green' : task.status === 'in_progress' ? 'badge-blue' : task.status === 'blocked' ? 'badge-red' : 'badge-gray'}"> {task.status === 'done' || task.status === 'completed' ? '완료' : task.status === 'in_progress' ? '진행중' : task.status === 'blocked' ? '차단됨' : '대기중'} </span> <span class="badge {task.priority === 'high' ? 'badge-red' : task.priority === 'medium' ? 'badge-yellow' : 'badge-green'}"> {task.priority === 'high' ? '높음' : task.priority === 'medium' ? '보통' : '낮음'} </span> </div> </div> </div> </div> {/each} </div> </div> {/if} <!-- 간접 연결 작업 --> {#if linkedTasks.indirect && linkedTasks.indirect.length > 0} <div> <h3 class="text-lg font-medium text-gray-900 mb-3">간접 연결 작업 ({linkedTasks.indirect.length}개)</h3> <div class="grid gap-3"> {#each linkedTasks.indirect as task} <div class="p-3 bg-purple-50 rounded-lg border border-purple-200"> <div class="flex items-center justify-between"> <div class="flex-1"> <h4 class="font-medium text-gray-900"> <a href="/tasks/{task.id}" class="text-purple-700 hover:text-purple-900 hover:underline"> {task.title} </a> </h4> <div class="flex items-center space-x-2 mt-1"> <span class="badge {task.status === 'done' || task.status === 'completed' ? 'badge-green' : task.status === 'in_progress' ? 'badge-blue' : task.status === 'blocked' ? 'badge-red' : 'badge-gray'}"> {task.status === 'done' || task.status === 'completed' ? '완료' : task.status === 'in_progress' ? '진행중' : task.status === 'blocked' ? '차단됨' : '대기중'} </span> <span class="text-xs text-purple-600">설계를 통한 연결</span> </div> </div> </div> </div> {/each} </div> </div> {/if} </div> {/if} </div> {/if} </div> <style> .badge { @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; } /* Badge color variations */ .badge-green { @apply bg-green-100 text-green-800; } .badge-blue { @apply bg-blue-100 text-blue-800; } .badge-red { @apply bg-red-100 text-red-800; } .badge-yellow { @apply bg-yellow-100 text-yellow-800; } .badge-purple { @apply bg-purple-100 text-purple-800; } .badge-gray { @apply bg-gray-100 text-gray-800; } /* Badge status colors */ .badge-status-green { @apply bg-green-100 text-green-800; } .badge-status-blue { @apply bg-blue-100 text-blue-800; } .badge-status-yellow { @apply bg-yellow-100 text-yellow-800; } .badge-status-gray { @apply bg-gray-100 text-gray-800; } .card { @apply bg-white rounded-lg shadow p-6; } .btn { @apply px-4 py-2 rounded-md font-medium transition-colors; } .btn-primary { @apply bg-blue-600 text-white hover:bg-blue-700; } .btn-secondary { @apply bg-gray-200 text-gray-900 hover:bg-gray-300; } </style>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/foswmine/workflow-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server